/*******************************************************************************
 * Copyright (c) 2014 Joel Thurlby.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Joel Thurlby - initial API and implementation
 ******************************************************************************/
package jat.xtext.gantt.ui.diagram;

import jat.xtext.gantt.ganttText.Date;
import jat.xtext.gantt.ganttText.FragmentMilestones;
import jat.xtext.gantt.ganttText.FragmentWorkPackages;
import jat.xtext.gantt.ganttText.Milestone;
import jat.xtext.gantt.ganttText.Project;
import jat.xtext.gantt.ganttText.Task;
import jat.xtext.gantt.ganttText.TaskState;
import jat.xtext.gantt.ganttText.WorkPackage;

import java.util.Calendar;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.nebula.widgets.ganttchart.GanttChart;
import org.eclipse.nebula.widgets.ganttchart.GanttEvent;
import org.eclipse.nebula.widgets.ganttchart.GanttSection;

public class GanttChartPreprocessor {
	private static final String SECTION_NAME_MILESTONES = "Milestones";
	private static final String TASK_STATE__STARTED = "STARTED";
	private static final String TASK_STATE__COMPLETE = "COMPLETE";

	/**
	 * The current GanttChart.
	 */
	private GanttChart m_ganttChart;

	/**
	 * The WorkPackage<->GanttEvent lookup table. Shall be cleared when the
	 * current GanttChart is updated.
	 */
	private Map<WorkPackage, GanttEvent> m_packLookUp = new LinkedHashMap<WorkPackage, GanttEvent>();

	/**
	 * The Task<->GanttEvent lookup table. Shall be cleared when the current
	 * GanttChart is updated.
	 */
	private Map<Task, GanttEvent> m_taskLookUp = new LinkedHashMap<Task, GanttEvent>();

	/**
	 * Allocates the artifacts defined within the GanttText project into the
	 * GanttChart.
	 * 
	 * @param proj
	 *            The GanttText project to allocate from.
	 * @param ganttChart
	 *            The GanttChart to populate.
	 */
	public void populateChart(Project proj, GanttChart ganttChart) {
		// RE-INIT
		m_packLookUp.clear();
		m_taskLookUp.clear();
		m_ganttChart = ganttChart;

		if (proj.getFragmentWorkPackages() == null) {
			// If there are no work packages defined, then the ganttChart will
			// be empty.
			return;
		}

		final FragmentWorkPackages fragWP = proj.getFragmentWorkPackages();
		final FragmentMilestones fragMS = proj.getFragmentMilestones();

		// WORK PACKAGES
		for (WorkPackage pack : fragWP.getWorkPackages()) {
			createCompositeEvent(pack);
		}

		// MILESTONES
		createMilestones(fragMS);

		// SECTIONS
		createSections(fragWP);

		// CONNECTIONS
		createConnections(fragWP);
	}

	/**
	 * Creates a GanttEvent for the task passed to this method. The task's start
	 * date is based on the work package's start date.
	 * 
	 * @param task
	 *            The task to create a GanttChart event for
	 * @param start
	 *            The start date of the event.
	 * @return The allocated GanttEvent.
	 */
	private GanttEvent createPrimitiveEvent(Task task, Calendar packageStart) {
		// START DATE
		Calendar start = getStartDate(packageStart, task);

		// END DATE
		Calendar end = GanttChartHelpers.getEndDate(start, task);

		int procentComplete = GanttChartHelpers.getPercentComplete(task);

		GanttEvent event = new GanttEvent(m_ganttChart, task, task.getName(), start, end, procentComplete);
		event.setMoveable(false);
		event.setResizable(false);

		m_taskLookUp.put(task, event);

		// TODO Make use of the task attribute "priority".

		return event;
	}

	private Calendar getStartDate(Calendar packageStart, Task task) {
		Calendar startDate = (Calendar) packageStart.clone();

		// This is the specified delay.
		if (task.getDelay() != null) {
			int numWorkingDays = GanttChartHelpers.getNumWorkingDays(task.getDelay().getDelay());
			int daysWorked = 0;
			while (daysWorked < numWorkingDays) {
				final int dayOfWeek = startDate.get(Calendar.DAY_OF_WEEK);
				if (dayOfWeek != Calendar.SATURDAY && dayOfWeek != Calendar.SUNDAY) {
					daysWorked++;
				}
				if (daysWorked < numWorkingDays) {
					startDate.add(Calendar.DATE, 1);
				}
			}
		}

		for (Task otherTask : task.getTaskDependencies()) {
			final Calendar otherTaskStartDate = getStartDate(packageStart, otherTask);
			final Calendar otherTaskEndDate = GanttChartHelpers.getEndDate(otherTaskStartDate, otherTask);
			if (otherTaskEndDate.compareTo(startDate) >= 0) {
				startDate = (Calendar) otherTaskEndDate.clone();
				startDate.add(Calendar.DATE, 1);
			}
		}
		return startDate;
	}

	/**
	 * Creates GanttEvents for the work package and work packages's tasks.
	 * 
	 * @param pack
	 *            The work package to create a GanttEvent for.
	 * @return The allocated GanttEvent.
	 */
	private GanttEvent createCompositeEvent(WorkPackage pack) {
		final GanttEvent scopeEvent = new GanttEvent(m_ganttChart, pack.getName());
		scopeEvent.setScope(true);
		scopeEvent.setData(pack);

		m_packLookUp.put(pack, scopeEvent);

		// EARLIEST START DATE
		if ((pack.getScheduling() != null) && (pack.getScheduling().getEarliestStartDate() != null)) {
			scopeEvent.setNoMoveBeforeDate(getCalendarDate(pack.getScheduling().getEarliestStartDate()));
		}

		// DUE DATE
		if ((pack.getScheduling() != null) && (pack.getScheduling().getDueDate() != null)) {
			scopeEvent.setNoMoveAfterDate(getCalendarDate(pack.getScheduling().getDueDate()));
		}

		// START DATE
		Calendar startDate = null;
		if ((pack.getScheduling() != null) && (pack.getScheduling().getPlannedStartDate() != null)) {
			startDate = getCalendarDate(pack.getScheduling().getPlannedStartDate());
		} else {
			// If no scheduling has been specified just use today's date.
			startDate = Calendar.getInstance();
		}
		scopeEvent.setStartDate(startDate);

		// SCOPE EVENTS (for tasks)
		for (Task task : pack.getTasks()) {
			GanttEvent taskEvent = createPrimitiveEvent(task, startDate);
			scopeEvent.addScopeEvent(taskEvent);
		}

		// END DATE
		if (scopeEvent.getLatestScopeEvent() != null) {
			scopeEvent.setEndDate(scopeEvent.getLatestScopeEvent().getEndDate());
		} else {
			Calendar endDate = (Calendar) startDate.clone();
			endDate.add(Calendar.DATE, GanttChartHelpers.getRequiredTime(pack));
			scopeEvent.setEndDate(endDate);
		}

		return scopeEvent;
	}

	/**
	 * Creates sections within the current GanttChart based on the top-level
	 * work packages. A top-level work package is one that has no dependencies
	 * to other work packages.
	 * 
	 * @param fragWP
	 *            The work package fragment of the current GanttText project.
	 */
	private void createSections(final FragmentWorkPackages fragWP) {
		for (WorkPackage pack : fragWP.getWorkPackages()) {
			if (pack.getPackageParent() == null) {
				// This is a top-level WP, create a section for it.
				GanttEvent parentEvent = m_packLookUp.get(pack);
				GanttSection section = new GanttSection(m_ganttChart, pack.getName());
				section.addGanttEvent(parentEvent);

				// TASKS
				for (Task tsk : pack.getTasks()) {
					section.addGanttEvent(m_taskLookUp.get(tsk));
				}

				// NESTED WORK PACKAGES
				for (WorkPackage nestedWp : getDepedentWorkPackages(pack)) {
					parentEvent.addScopeEvent(m_packLookUp.get(nestedWp));
					section.addGanttEvent(m_packLookUp.get(nestedWp));
					// Also add the tasks of the nested work packages
					for (Task task : nestedWp.getTasks()) {
						section.addGanttEvent(m_taskLookUp.get(task));
					}
				}
			}
		}
	}

	/**
	 * Returns all packages that are directly or indirectly dependent on the
	 * parent package.
	 * 
	 * @param The
	 *            parent work package
	 * @return All nested packages.
	 */
	private Set<WorkPackage> getDepedentWorkPackages(WorkPackage parentPackage) {
		Set<WorkPackage> nestedPackages = new LinkedHashSet<WorkPackage>();

		for (WorkPackage wp : m_packLookUp.keySet()) {
			if ((wp.getPackageParent() != null) && wp.getPackageParent().equals(parentPackage)) {
				nestedPackages.add(wp);
				nestedPackages.addAll(getDepedentWorkPackages(wp));
			}
		}

		return nestedPackages;
	}

	/**
	 * Creates checkpoints within the current GanttChart based on the milestones
	 * defined within the GanttText project. The milestone's date is based on
	 * the task state conditions defined within milestone instance.
	 * 
	 * @param fragMS
	 *            The miles stones fragment defined within the current GanttText
	 *            project.
	 */
	private void createMilestones(final FragmentMilestones fragMS) {
		if (fragMS == null) {
			return;
		}

		GanttSection msSection = new GanttSection(m_ganttChart, SECTION_NAME_MILESTONES);

		for (Milestone ms : fragMS.getMilestones()) {
			// MILESTONE DATE
			Calendar msDate = null;
			boolean isFirst = true;
			for (TaskState taskState : ms.getTaskStates()) {
				Task task = taskState.getTask();
				String state = taskState.getTaskState();

				Calendar eventDate = null;
				if (TASK_STATE__STARTED.equals(state)) {
					eventDate = m_taskLookUp.get(task).getStartDate();
				} else if (TASK_STATE__COMPLETE.equals(state)) {
					eventDate = m_taskLookUp.get(task).getEndDate();
				}

				if (isFirst) {
					msDate = eventDate;
				}
				
				if (msDate != null && msDate.compareTo(eventDate) < 0) {
					msDate = eventDate;
				}

				isFirst = false;
			}

			GanttEvent event = new GanttEvent(m_ganttChart, null, ms.getName(), msDate, msDate, 0);
			event.setCheckpoint(true);
			event.setData(ms);
			msSection.addGanttEvent(event);
		}
	}

	/**
	 * Creates connections between work packages and between tasks that have
	 * dependencies defined for them.
	 * 
	 * @param fragWP
	 *            The work package fragment contained within a GanttText
	 *            project.
	 */
	private void createConnections(final FragmentWorkPackages fragWP) {
		for (WorkPackage pack : fragWP.getWorkPackages()) {
			GanttEvent event = m_packLookUp.get(pack);

			// WORK PACKAGE DEPENDENCIES
			for (WorkPackage sourcePack : pack.getPackageDependencies()) {
				GanttEvent sourceEvent = m_packLookUp.get(sourcePack);
				if (sourceEvent != null) {
					m_ganttChart.addConnection(sourceEvent, event);
				}
			}

			// TASK DEPENDENCIES
			for (Task task : pack.getTasks()) {
				GanttEvent taskEvent = m_taskLookUp.get(task);
				for (Task sourceTask : task.getTaskDependencies()) {
					GanttEvent sourceEvent = m_taskLookUp.get(sourceTask);
					if (sourceEvent != null) {
						m_ganttChart.addConnection(sourceEvent, taskEvent);
					}
				}
			}
		}
	}

	/**
	 * @return The GanttEvent event for an allocated task.
	 */
	public GanttEvent getEvent(Task task) {
		return m_taskLookUp.get(task);
	}

	/**
	 * @return The GanttEvent for an allocated work package.
	 */
	public GanttEvent getEvent(WorkPackage pack) {
		return m_packLookUp.get(pack);
	}

	/**
	 * Helper method to convert a ganttText Date into a Calendar object.
	 * 
	 * @param date
	 *            The ganttText Date to convert.
	 * @return The corresponding Calendar object.
	 */
	private Calendar getCalendarDate(Date date) {
		Calendar calDate = Calendar.getInstance();
		calDate.set(date.getYear(), date.getMonth() - 1, date.getDay());
		return calDate;
	}
}
